/*******************************************************************************
 * Copyright (c) 2010-2011 Robert Fraser                                       *
 *                                                                             *
 * Permission is hereby granted, free of charge, to any person obtaining a     *
 * copy of this software and associated documentation files (the "Software"),  *
 * to deal in the Software without restriction, including without limitation   *
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,    *
 * and/or sell copies of the Software, and to permit persons to whom the       *
 * Software is furnished to do so, subject to the following conditions:        *
 *                                                                             *
 *   - The above copyright notice and this permission notice shall be included *
 *     in all copies or substantial portions of the Software.                  *
 *                                                                             *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR  *
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,    *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER      *
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING     *
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER         *
 * DEALINGS IN THE SOFTWARE.                                                   *
 *******************************************************************************/

using System;
using System.Windows.Media;
using zeal.util;

namespace zeal.ui.util
{
    internal struct ColorHsl
    {
        public double hue;
        public double sat;
        public double lit;

        public ColorHsl(double hue, double sat, double lit)
        {
            this.hue = hue;
            this.sat = sat;
            this.lit = lit;
        }

        public ColorHsl(Color c)
        {
            if(c.R == c.G && c.G == c.B)
            {
                // If they're all the same, it's a shade of gray -- skip all the following processing
                hue = 0;
                sat = 0;
                lit = c.R / 256.0;
                return;
            }

            double r = c.R / 256.0;
            double g = c.G / 256.0;
            double b = c.B / 256.0;

            // Find the largest and smallest of the color values, which gives si possible
            // sextants from which to find the HSL
            double max = Math.Max(r, Math.Max(g, b));
            double min = Math.Min(r, Math.Min(g, b));

            // First we need to find the chroma, which is the distance from the center of the
            // color hexagon, which can be found as the difference between the max and the min
            double chroma = max - min;

            // Next, find the hue which is the radial projection about the color hexagon (that is,
            // a hue of 0 is 0 degrees, and 1 is 360 degrees (= 0), where each 60 degrees
            // corresponds to a side of the hexagon. The verticies of the hexagon, from 0 degrees to
            // 360 degrees, are: red, yellow, green, cyan, blue, magenta -- so the edges are
            // firey yellow/orange, ugly goblin skin, serene carribean seaside sex, scary 7-foot-tall
            // bjork sex, the color of a furry cape for ordering servants around, and
            // the color of the blood that's all over my floor
            if (r == max)      hue = ((g - b) / chroma) + 0;
            else if (g == max) hue = ((b - r) / chroma) + 2;
            else               hue = ((r - g) / chroma) + 4;
            hue /= 6;
            if (hue < 0)
                hue = hue + 1;

            // Lightness is the average of the minimum and maximum colors. This makes intuitive sense --
            // #804020 is about half as bright as #FF8040, while having the same hue/sat
            lit = (min + max) / 2;

            // This is simply an arbitrary mapping of chormacity such that sat = 0 corresponds to gray
            // and sat = 1 corresponds to full color
            sat = chroma / (lit < 0.5 ? 2 * lit : 2 - (2 * lit));
        }

        public Color toColor()
        {
            if (!hue.isValid() || !sat.isValid() || !lit.isValid())
                throw new InvalidOperationException("HSL values cannot be NaN or infinity");

            // Use sat/lit values are in the range [0, 1] -- values outside this range aren't
            // nesecarily invalid (HDR!), but they cannot be represented in the RGB color
            // space
            double s = clamp(sat);
            double l = clamp(lit);

            // If saturation == 0, it's a shade of gray
            if(s == 0)
            {
                byte rgb = (byte) l;
                return Color.FromRgb(rgb, rgb, rgb);
            }

            // Okay, time to reverse the above process. First, use the saturation and lightness to calculate the
            // chromacity in [0, 1]
            double c = s * (l < 0.5 ? 2 * l : (2 - 2 * l));

            // Now find which sextant of the hexagon its in, and get the "fractional" part
            double h6 = hue * 6;
            int sextant = (int) Math.Floor(h6);

            // The intermediate value (one that's not min/max) is defined as chroma * (1 - abs(h6 % 2 - 1)):
            // First get the "fractional" part, which is how far along the hexagon's edge we are
            double frac = h6 - sextant;

            // h6 % 2 < 1 in sextants 0, 2, and 4, and 1 - abs(h6 % 2 - 1) = 1 - abs(frac - 1) = 1 - (1 - frac) = frac
            double x = c * frac;

            //  h6 % 2 > 1 in sextants 1, 3, and 5, and 1 - abs(h6 % 2 - 1) = 1 - abs(frac + 1 - 1) = 1 - frac
            double y = c * (1 - frac);

            // If we're over or under 6, then "wrap around" the circle
            sextant %= 6;

            // Next find some temporary red/green/blue values (which will need to be lightened later)
            // based on our hexagonal sextant. Put 0 for the temporary min value and the chroma for the max
            double r, g, b;
            switch(sextant)
            {
                case 0: r = c; g = x; b = 0; break; // Red-yellow
                case 1: r = y; g = c; b = 0; break; // Yellow-green
                case 2: r = 0; g = c; b = x; break; // Green-cyan
                case 3: r = 0; g = y; b = c; break; // Cyan-blue
                case 4: r = x; g = 0; b = c; break; // Blue-magenta
                case 5: r = c; g = 0; b = y; break; // Magenta-red
                default: throw new InvalidOperationException("Your computer's mod function is broken");
            }

            // Now lighten these values up a bit, by the "unsaturated" portion of the color
            double m = l - (c / 2);
            return Color.FromRgb(toByte(r + m), toByte(g + m), toByte(b + m));
        }

        private static byte toByte(double d) { return (byte) (clamp(d) * 255); }
        private static double clamp(double d) { return d < 0 ? 0 : d > 1 ? 1 : d; }
    }
}